<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
    </head>

    <body>
        <div id="TemplateConfigPage" data-role="page" class="page type-interior pluginConfigurationPage" data-require="emby-input,emby-button,emby-select,emby-checkbox,emby-linkbutton">
            <div data-role="content">
                <style>
                    summary {
                        cursor: pointer;
                        padding: 10px;
                        width: inherit;
                        margin: auto;
                        border: none;
                        text-align: center;
                        font-size: 1em;
                        outline: 2px solid rgba(155, 155, 155, 0.5);
                    }
                    h3.checkboxListLabel {
                        font-size: 1em;
                        margin-bottom: 4px;
                    }
                </style>
                <div class="content-primary">
                    <form id="FingerprintConfigForm">
                        <fieldset class="verticalSection-extrabottompadding">
                            <legend>Analysis</legend>

                            <div class="checkboxContainer checkboxContainer-withDescription">
                                <label class="emby-checkbox-label">
                                    <input id="AutoDetectIntros" type="checkbox" is="emby-checkbox" />
                                    <span>Automatically Analyze New Media</span>
                                </label>

                                <div class="fieldDescription">
                                    If enabled, new media will be automatically analyzed for skippable segments when added to the library
                                    <br />
                                    <br />
                                    Note: To configure the scheduled task, see <a is="emby-linkbutton" class="button-link" href="#/dashboard/tasks">scheduled tasks</a>.
                                </div>
                            </div>

                            <div class="checkboxContainer checkboxContainer-withDescription">
                                <label class="emby-checkbox-label">
                                    <input id="UpdateMediaSegments" type="checkbox" is="emby-checkbox" />
                                    <span>Update Missing Segments During Scan</span>
                                </label>

                                <div class="fieldDescription">
                                    Enable this option to update media segments for any uncached media during a library scan. <br />
                                    This includes recently added, modified, or previously skipped (but not ignored) files.<br />
                                    <b>Warning:</b> This should be disabled if you're using media segment providers other than Intro Skipper.
                                </div>
                            </div>

                            <div class="checkboxContainer checkboxContainer-withDescription">
                                <label class="emby-checkbox-label">
                                    <input id="SelectAllLibraries" type="checkbox" is="emby-checkbox" />
                                    <span>Enable analysis for all libraries (uncheck to limit analysis to specific libraries)</span>
                                </label>
                                <div class="folderAccessListContainer" style="margin-bottom: -1em">
                                    <div class="folderAccess">
                                        <h3 class="checkboxListLabel">Limit analysis to the following libraries</h3>
                                        <div class="checkboxList paperList" style="padding: 0.5em 1em" id="libraryCheckboxes"></div>
                                    </div>
                                    <label class="inputLabel" for="SelectedLibraries"></label>
                                    <input id="SelectedLibraries" type="hidden" is="emby-input" />
                                </div>
                            </div>

                            <div class="inputContainer" id="excludedSeries">
                                <label class="inputLabel inputLabelUnfocused" for="ExcludeSeries"> Exclude series </label>
                                <input id="ExcludeSeries" type="text" is="emby-input" />
                                <div class="fieldDescription">Exclude series from analysis. Enter a comma-separated list of series names to exclude.</div>
                            </div>

                            <div class="checkboxContainer" style="display: flex; flex-direction: row; flex-wrap: nowrap; align-items: center;">
                                <span style="font-weight: 600; margin-right: 1em; white-space: nowrap;">Analyze for:</span>
                                <label class="emby-checkbox-label">
                                    <input id="ScanIntroduction" type="checkbox" is="emby-checkbox" />
                                    <span>Introductions</span>
                                </label>
                                <label class="emby-checkbox-label">
                                    <input id="ScanCredits" type="checkbox" is="emby-checkbox" />
                                    <span>Credits</span>
                                </label>
                                <label class="emby-checkbox-label">
                                    <input id="ScanRecap" type="checkbox" is="emby-checkbox" />
                                    <span>Recaps</span>
                                </label>
                                <label class="emby-checkbox-label">
                                    <input id="ScanPreview" type="checkbox" is="emby-checkbox" />
                                    <span>Previews</span>
                                </label>
                            </div>

                            <div class="checkboxContainer">
                                <label class="emby-checkbox-label">
                                    <input id="AnalyzeMovies" type="checkbox" is="emby-checkbox" />
                                    <span>Analyze Movies</span>
                                </label>
                            </div>

                            <div class="checkboxContainer checkboxContainer-withDescription">
                                <label class="emby-checkbox-label">
                                    <input id="AnalyzeSeasonZero" type="checkbox" is="emby-checkbox" />
                                    <span>Analyze Season 0 (Specials / Extras)</span>
                                </label>

                                <div class="fieldDescription">Note: Shows containing both a specials and extra folder will identify extras as season 0 and ignore specials, regardless of this setting.</div>
                            </div>

                            <details id="intro_reqs">
                                <summary>Modify Analysis Parameters</summary>

                                <br />
                                <div class="checkboxContainer checkboxContainer-withDescription">
                                    <label class="emby-checkbox-label">
                                        <input id="PreferChromaprint" type="checkbox" is="emby-checkbox" />
                                        <span>Prefer Chromaprint Analysis</span>
                                    </label>
                                    <div class="fieldDescription">Only use chromaprint for analysis, unless it is not available. Setting an analysis mode in the advanced options will override this setting.</div>
                                </div>

                                <div class="checkboxContainer checkboxContainer-withDescription">
                                    <label class="emby-checkbox-label">
                                        <input id="FullLengthChapters" type="checkbox" is="emby-checkbox" />
                                        <span>Ignore duration limits for chapters</span>
                                    </label>
                                    <div class="fieldDescription">Allow segments to extend to the end of a chapter when the marker exceeds other user settings, such as percentage or duration.</div>
                                </div>

                                <div class="inputContainer">
                                    <label class="inputLabel inputLabelUnfocused" for="AnalysisPercent"> Percent of media to analyze </label>
                                    <input id="AnalysisPercent" type="number" is="emby-input" min="1" max="90" />
                                    <div class="fieldDescription">Analysis will be limited to this percentage of each item's runtime. For example, a value of 25 (the default) will limit analysis to the first quarter of each item.</div>
                                </div>

                                <div class="inputContainer">
                                    <label class="inputLabel inputLabelUnfocused" for="AnalysisLengthLimit"> Maximum runtime to analyze (in minutes) </label>
                                    <input id="AnalysisLengthLimit" type="number" is="emby-input" min="1" />
                                    <div class="fieldDescription">Analysis will be limited to this amount of each item's runtime. For example, a value of 10 (the default) will limit analysis to the first 10 minutes of each item.</div>
                                </div>

                                <p>The amount of each item's content that will be analyzed is determined using the percentage and maximum runtime. The minimum of (duration * percent, maximum runtime) is the amount that will be analyzed.</p>
                                <p>If the percentage or maximum runtime settings are modified, the cached fingerprints and timestamps for each series, season, or movie you want to analyze with the modified settings <b>will have to be recreated</b>.</p>
                                <p>Increasing either of the above settings will cause episode analysis to take much longer.</p>
                                <br />

                                <div class="inputContainer">
                                    <label class="inputLabel inputLabelUnfocused" for="MinimumIntroDuration"> Minimum introduction duration (in seconds) </label>
                                    <input id="MinimumIntroDuration" type="number" is="emby-input" min="1" />
                                    <div class="fieldDescription">Segments or similar sounding audio which is shorter than this duration will not be considered an introduction.</div>
                                </div>

                                <div class="inputContainer">
                                    <label class="inputLabel inputLabelUnfocused" for="MaximumIntroDuration"> Maximum introduction duration (in seconds) </label>
                                    <input id="MaximumIntroDuration" type="number" is="emby-input" min="1" />
                                    <div class="fieldDescription">Segments or similar sounding audio which is longer than this duration will not be considered an introduction.</div>
                                </div>

                                <div class="inputContainer">
                                    <label class="inputLabel inputLabelUnfocused" for="MinimumCreditsDuration"> Minimum credits duration (in seconds) </label>
                                    <input id="MinimumCreditsDuration" type="number" is="emby-input" min="1" />
                                    <div class="fieldDescription">Segments or similar sounding audio which is shorter than this duration will not be considered credits.</div>
                                </div>

                                <div class="inputContainer">
                                    <label class="inputLabel inputLabelUnfocused" for="MaximumCreditsDuration"> Maximum credits duration (in seconds) </label>
                                    <input id="MaximumCreditsDuration" type="number" is="emby-input" min="1" />
                                    <div class="fieldDescription">Segments or similar sounding audio which is longer than this duration will not be considered credits.</div>
                                </div>

                                <div class="inputContainer" id="movieCreditsDuration">
                                    <label class="inputLabel inputLabelUnfocused" for="MaximumMovieCreditsDuration"> Maximum movie credits duration (in seconds) </label>
                                    <input id="MaximumMovieCreditsDuration" type="number" is="emby-input" min="1" />
                                    <div class="fieldDescription">Segments longer than this duration will not be considered movie credits.</div>
                                </div>

                                <div id="RecapPreviewDurations">
                                    <div class="inputContainer">
                                        <label class="inputLabel inputLabelUnfocused" for="MinimumRecapDuration"> Minimum recap duration (in seconds) </label>
                                        <input id="MinimumRecapDuration" type="number" is="emby-input" min="1" />
                                        <div class="fieldDescription">Segments which are shorter than this duration will not be considered a recap.</div>
                                    </div>

                                    <div class="inputContainer">
                                        <label class="inputLabel inputLabelUnfocused" for="MaximumRecapDuration"> Maximum recap duration (in seconds) </label>
                                        <input id="MaximumRecapDuration" type="number" is="emby-input" min="1" />
                                        <div class="fieldDescription">Segments which are longer than this duration will not be considered a recap.</div>
                                    </div>

                                    <div class="inputContainer">
                                        <label class="inputLabel inputLabelUnfocused" for="MinimumPreviewDuration"> Minimum preview duration (in seconds) </label>
                                        <input id="MinimumPreviewDuration" type="number" is="emby-input" min="1" />
                                        <div class="fieldDescription">Segments which are shorter than this duration will not be considered a preview.</div>
                                    </div>

                                    <div class="inputContainer">
                                        <label class="inputLabel inputLabelUnfocused" for="MaximumPreviewDuration"> Maximum preview duration (in seconds) </label>
                                        <input id="MaximumPreviewDuration" type="number" is="emby-input" min="1" />
                                        <div class="fieldDescription">Segments which are longer than this duration will not be considered a preview.</div>
                                    </div>
                                </div>
                                <br />
                            </details>

                            <details id="adjustment">
                                <summary>Detection Adjustment Options</summary>

                                <br />
                                <div class="checkboxContainer checkboxContainer-withDescription">
                                    <label class="emby-checkbox-label">
                                        <input id="AdjustIntroBasedOnSilence" type="checkbox" is="emby-checkbox" />
                                        <span>Enable silence detection</span>
                                    </label>

                                    <div class="fieldDescription">When enabled, segment endpoints will be adjusted to the nearest silence point.</div>
                                </div>
                                <div id="silenceSettings">
                                    <div class="inputContainer">
                                        <label class="inputLabel inputLabelUnfocused" for="SilenceDetectionMaximumNoise"> Noise tolerance </label>
                                        <input id="SilenceDetectionMaximumNoise" type="number" is="emby-input" min="-90" max="0" />
                                        <div class="fieldDescription">Noise tolerance in negative decibels.</div>
                                    </div>

                                    <div class="inputContainer">
                                        <label class="inputLabel inputLabelUnfocused" for="SilenceDetectionMinimumDuration"> Minimum silence duration </label>
                                        <input id="SilenceDetectionMinimumDuration" type="number" is="emby-input" min="0" step="0.01" />
                                        <div class="fieldDescription">Minimum silence duration in seconds before adjusting introduction end time.</div>
                                    </div>
                                    <br />
                                </div>
                                <div class="checkboxContainer checkboxContainer-withDescription">
                                    <label class="emby-checkbox-label">
                                        <input id="SnapToKeyframe" type="checkbox" is="emby-checkbox" />
                                        <span>Enable keyframe snapping</span>
                                    </label>

                                    <div class="fieldDescription">When enabled, segment endpoints will be adjusted to the nearest video keyframe for smoother seek transitions during skipping.</div>
                                </div>
                                <div class="checkboxContainer checkboxContainer-withDescription">
                                    <label class="emby-checkbox-label">
                                        <input id="AdjustIntroBasedOnChapters" type="checkbox" is="emby-checkbox" />
                                        <span>Enable chapter snapping</span>
                                    </label>

                                    <div class="fieldDescription">When enabled, segment start and end times will be adjusted to the nearest chapter boundary.</div>
                                </div>

                                <div class="inputContainer">
                                    <label class="inputLabel inputLabelUnfocused" for="AdjustWindowInward">Adjustment window (inward)</label>
                                    <input id="AdjustWindowInward" type="number" is="emby-input" min="0" />
                                    <div class="fieldDescription">Maximum number of seconds to search toward a segment's interior for adjustment points (like chapter boundaries, silence, or keyframes). Used to tighten segment boundaries.</div>
                                </div>

                                <div class="inputContainer">
                                    <label class="inputLabel inputLabelUnfocused" for="AdjustWindowOutward">Adjustment window (outward)</label>
                                    <input id="AdjustWindowOutward" type="number" is="emby-input" min="0" />
                                    <div class="fieldDescription">Maximum number of seconds to search away from a segment for adjustment points (like chapter boundaries, silence, or keyframes). Used to expand segment boundaries.</div>
                                </div>

                                <div class="inputContainer">
                                    <label class="inputLabel inputLabelUnfocused" for="EndSnapThreshold"> Snap to episode start/end threshold </label>
                                    <input id="EndSnapThreshold" type="number" is="emby-input" min="0" />
                                    <div class="fieldDescription">If a segment's start or end is within this many seconds of the episode's start or end, it will be automatically adjusted (snapped) to match the episode boundary. Set to 0 to disable snapping.</div>
                                </div>

                                <fieldset>
                                  <legend>Segment Offset Adjustment</legend>
                                  <div class="inputContainer">
                                    <label class="inputLabel inputLabelUnfocused" for="IntroStartOffset">Intro Start Offset (seconds)</label>
                                    <input id="IntroStartOffset" is="emby-input" type="number" min="0" value="0" step="0.5" />
                                    <div class="fieldDescription">
                                      Default: 0. Example: If set to 3, the first 3 seconds of the intro will play before skipping.
                                    </div>
                                  </div>
                                  <div class="inputContainer">
                                    <label for="IntroEndOffset">Intro End Offset (seconds)</label>
                                    <input id="IntroEndOffset" is="emby-input" type="number" min="0" value="0" step="0.5" />
                                    <div class="fieldDescription">
                                      Default: 0. Example: If set to 3, playback will resume 3 seconds before the end of the intro.
                                    </div>
                                  </div>
                                </fieldset>
                                <br />
                            </details>

                            <details id="blackframe">
                                <summary>Black Frame Detection Options</summary>

                                <br />
                                <div class="checkboxContainer checkboxContainer-withDescription">
                                    <label class="emby-checkbox-label">
                                        <input id="UseAlternativeBlackFrameAnalyzer" type="checkbox" is="emby-checkbox" />
                                        <span>Use alternative black frame analyzer (experimental)</span>
                                    </label>

                                    <div class="fieldDescription">If enabled, the alternative black frame analyzer will be used. This analyzer is experimental and may not work as expected.</div>
                                </div>

                                <div class="checkboxContainer checkboxContainer-withDescription" id="ChapterMarkersBlackFrameSetting">
                                    <label class="emby-checkbox-label">
                                        <input id="UseChapterMarkersBlackFrame" type="checkbox" is="emby-checkbox" />
                                        <span>Use chapter markers for credits detection</span>
                                    </label>

                                    <div class="fieldDescription">If enabled, chapter markers will be used to identify credits segments. Tries to detect credits by looking for a black frames close to chapter markers.</div>
                                    <br />
                                </div>

                                <div class="inputContainer">
                                    <label class="inputLabel inputLabelUnfocused" for="BlackFrameMinimumPercentage"> Minimum percentage of black pixels </label>
                                    <input id="BlackFrameMinimumPercentage" type="number" is="emby-input" min="0" max="100" />
                                    <div class="fieldDescription">Minimum percentage of black pixels in a frame before it is considered a black frame. Defaults to 85.</div>
                                </div>

                                <div class="inputContainer">
                                    <label class="inputLabel inputLabelUnfocused" for="BlackFrameThreshold"> Black frame threshold </label>
                                    <input id="BlackFrameThreshold" type="number" is="emby-input" min="16" max="255" />
                                    <div class="fieldDescription">The threshold below which a pixel value is considered black. Defaults to 32.</div>
                                </div>
                            </details>

                            <details id="chapters">
                                <summary>Chapter Detection Options</summary>

                                <br />
                                <div class="inputContainer">
                                    <label class="inputLabel inputLabelUnfocused" for="ChapterAnalyzerIntroductionPattern"> Introductions </label>
                                    <input id="ChapterAnalyzerIntroductionPattern" type="text" placeholder="(^|\s)(Intro|Introduction|OP|Opening)(?!\sEnd)(\s|$)" is="emby-input" />
                                    <div class="fieldDescription">Enter a regular expression to detect introduction chapters. <br />Default: <code>(^|\s)(Intro|Introduction|OP|Opening)(?!\sEnd)(\s|$)</code></div>
                                </div>

                                <div class="inputContainer">
                                    <label class="inputLabel inputLabelUnfocused" for="ChapterAnalyzerEndCreditsPattern"> Credits </label>
                                    <input id="ChapterAnalyzerEndCreditsPattern" type="text" placeholder="(^|\s)(Credits?|ED|Ending|Outro)(?!\sEnd)(\s|$)" is="emby-input" />
                                    <div class="fieldDescription">Enter a regular expression to detect credits chapters. <br />Default: <code>(^|\s)(Credits?|ED|Ending|Outro)(?!\sEnd)(\s|$)</code></div>
                                </div>

                                <div class="inputContainer">
                                    <label class="inputLabel inputLabelUnfocused" for="ChapterAnalyzerPreviewPattern"> Preview </label>
                                    <input id="ChapterAnalyzerPreviewPattern" type="text" placeholder="(^|\s)(Preview|PV|Sneak\s?Peek|Coming\s?(Up|Soon)|Next\s+(time|on|episode)|Extra|Teaser|Trailer)(?!\sEnd)(\s|:|$)" is="emby-input" />
                                    <div class="fieldDescription">Enter a regular expression to detect preview chapters. <br />Default: <code>(^|\s)(Preview|PV|Sneak\s?Peek|Coming\s?(Up|Soon)|Next\s+(time|on|episode)|Extra|Teaser|Trailer)(?!\sEnd)(\s|:|$)</code></div>
                                </div>

                                <div class="inputContainer">
                                    <label class="inputLabel inputLabelUnfocused" for="ChapterAnalyzerRecapPattern"> Recaps </label>
                                    <input id="ChapterAnalyzerRecapPattern" type="text" placeholder="(^|\s)(Re?cap|Sum{1,2}ary|Prev(ious(ly)?)?|(Last|Earlier)(\s\w+)?|Catch[ -]up)(?!\sEnd)(\s|:|$)" is="emby-input" />
                                    <div class="fieldDescription">Enter a regular expression to detect recap chapters. <br />Default: <code>(^|\s)(Re?cap|Sum{1,2}ary|Prev(ious(ly)?)?|(Last|Earlier)(\s\w+)?|Catch[ -]up)(?!\sEnd)(\s|:|$)</code></div>
                                </div>
                            </details>

                            <details id="detection">
                                <summary>FFmpeg Process / Advanced</summary>

                                <br />
                                <div class="inputContainer">
                                    <label class="inputLabel inputLabelUnfocused" for="MaxParallelism"> Maximum degree of parallelism </label>
                                    <input id="MaxParallelism" type="number" is="emby-input" min="1" />
                                    <div class="fieldDescription">Maximum number of simultaneous async episode analysis operations.</div>
                                </div>

                                <div class="selectContainer">
                                    <label class="selectLabel" for="ProcessPriority">FFmpeg Priority</label>
                                    <select is="emby-select" id="ProcessPriority" class="emby-select-withcolor emby-select">
                                        <option value="Idle">Idle</option>

                                        <option value="BelowNormal">Below Normal</option>

                                        <option value="Normal">Normal</option>

                                        <option value="AboveNormal">Above Normal</option>

                                        <option value="High">High</option>

                                        <option value="RealTime">Highest</option>
                                    </select>

                                    <div class="fieldDescription">Sets the relative priority of the analysis FFmpeg process to other parallel operations (i.e. transcoding, chapter detection, etc).</div>
                                </div>

                                <div class="inputContainer">
                                    <label class="inputLabel inputLabelUnfocused" for="ProcessThreads"> FFmpeg Threads </label>
                                    <input id="ProcessThreads" type="number" is="emby-input" min="0" max="16" />
                                    <div class="fieldDescription">
                                        Number of simultaneous processes to use for FFmpeg operations.
                                        <br />
                                        This value is most often defined as 1 thread per CPU core, but setting a value of 0 (default) will use the maximum threads available.
                                    </div>
                                </div>

                                <div class="checkboxContainer checkboxContainer-withDescription">
                                    <label class="emby-checkbox-label">
                                        <input id="CacheFingerprints" type="checkbox" is="emby-checkbox" />
                                        <span>Cache analysis fingerprints</span>
                                    </label>

                                    <div class="fieldDescription">
                                        <b>Debug use only!</b> If checked, fingerprints will be saved on the filesystem to improve analysis speed.
                                        <br />
                                        <b>WARNING: Disabling the cache will cause all fingerprints to be recreated and not saved during analysis.</b>
                                        <br />
                                    </div>
                                </div>
                            </details>

                            <p>
                                <b style="color: orange">Changing analysis or detection requires regenerating media segments before changes take effect.</b>
                                <br />
                                Per the jellyfin MediaSegments API, records must be updated individually and may be slow to regenerate.
                            </p>

                            <div class="checkboxContainer checkboxContainer-withDescription">
                                <label class="emby-checkbox-label">
                                    <input id="RebuildMediaSegments" type="checkbox" is="emby-checkbox" />
                                    <span>Regenerate All Media Segments on Next Run</span>
                                </label>

                                <div class="fieldDescription">When enabled, this option will <b>overwrite all existing media segments</b> with your current Intro Skipper timestamps during the next analysis. Upon completion, this option will be automatically disabled.</div>
                            </div>

                            <p align="center" style="font-size: 0.75em">EDL file generation has been removed. Please use the <a href="https://github.com/intro-skipper/jellyfin-plugin-edl">EDL plugin</a>.</p>
                        </fieldset>

                        <div class="checkboxContainer checkboxContainer-withDescription">
                            <label class="emby-checkbox-label">
                                <input id="PluginSkip" type="checkbox" is="emby-checkbox" />
                                <span>Enable <b style="color: red">Server-side</b> Auto Skip</span>
                            </label>
                        </div>

                        <div id="ServerSkipSettings" style="display: none">
                            <fieldset class="verticalSection-extrabottompadding">
                                <legend>Playback</legend>

                                <div class="checkboxContainer checkboxContainer-withDescription">
                                    <label class="emby-checkbox-label">
                                        <input id="AutoSkip" type="checkbox" is="emby-checkbox" />
                                        <span>Automatically Skip for All Clients</span>
                                    </label>
                                </div>

                                <div class="AutoSkipClientListContainer">
                                    <div class="AutoSkipClientList">
                                        <h3 class="checkboxListLabel">Limit auto skip to the following clients</h3>
                                        <div class="checkboxList paperList" style="padding: 0.5em 1em" id="autoSkipCheckboxes"></div>
                                    </div>
                                    <label class="inputLabel" for="ClientList"></label>
                                    <input id="ClientList" type="hidden" is="emby-input" />
                                </div>

                                <div class="AutoSkipTypeListContainer">
                                    <div class="AutoSkipTypeList">
                                        <h3 class="checkboxListLabel">Auto skip the following types</h3>
                                        <div class="checkboxList paperList" style="padding: 0.5em 1em" id="autoSkipTypeCheckboxes"></div>
                                    </div>
                                    <label class="inputLabel" for="TypeList"></label>
                                    <input id="TypeList" type="hidden" is="emby-input" />
                                </div>

                                <div id="divSkipFirstEpisode" class="checkboxContainer checkboxContainer-withDescription">
                                    <label class="emby-checkbox-label">
                                        <input id="SkipFirstEpisode" type="checkbox" is="emby-checkbox" />
                                        <span>Play Segments for First Episode of a Season</span>
                                    </label>

                                    <div class="fieldDescription">If checked, auto skip will play the segments of the first episode in a season.</div>
                                </div>

                                <div id="divAutoSkipDelay" class="inputContainer">
                                    <label class="inputLabel inputLabelUnfocused" for="AutoSkipDelay"> Auto skip delay (in seconds) </label>
                                    <input id="AutoSkipDelay" type="number" is="emby-input" min="0" />
                                    <div class="fieldDescription">Seconds at the start of a segment that should be played before skipping. Defaults to 0.</div>
                                </div>

                                <div id="divAutoSkipNotificationText" class="inputContainer">
                                    <label class="inputLabel" for="AutoSkipNotificationText"> Auto skip notification message </label>
                                    <input id="AutoSkipNotificationText" type="text" is="emby-input" />
                                    <div class="fieldDescription">Message shown after automatically skipping a segment. Leave blank to disable notification. <br />Available variables: %segmenttype, %start, %end, %duration</div>
                                    <p>
                                        <b style="color: orange">This setting does not apply to Media Segment Actions in Jellyfin 10.10 and compatible clients.</b>
                                    </p>
                                </div>
                            </fieldset>
                        </div>

                        <div>
                            <button is="emby-button" type="submit" class="raised button-submit block emby-button">
                                <span>Save</span>
                            </button>
                        </div>
                        <br />

                        <fieldset class="verticalSection-extrabottompadding">
                            <legend>Advanced</legend>

                            <details id="visualizer">
                                <summary>Edit Timestamps & Fingerprints</summary>

                                <br />
                                <label class="inputLabel" for="troubleshooterShow">Select TV series / movie to manage</label>
                                <select is="emby-select" id="troubleshooterShow" class="emby-select-withcolor emby-select"></select>
                                <div id="seasonSelection" style="display: none">
                                    <p>
                                        <label class="inputLabel" for="troubleshooterSeason">Select season to manage</label>
                                        <select is="emby-select" id="troubleshooterSeason" class="emby-select-withcolor emby-select"></select>
                                    </p>
                                </div>

                                <div id="analyzerActionsSection" style="display: none">
                                    <br />
                                    <h3 style="margin: 0">Analyzer actions</h3>
                                    <p style="margin: 0">
                                        Choose how segments should be analyzed for this season.<br />
                                        <i>
                                            Default uses all available detection methods (Chromaprint, Chapter, and BlackFrame for credits).<br />
                                            Select specific methods to limit analysis, or None to skip detection entirely.
                                        </i>
                                    </p>
                                    <br />

                                    <div id="analyzerActionsContainer" style="display: flex; align-items: center; gap: 1.5em">
                                        <label for="actionRecap" style="width: 23%">
                                            <span>Recap analysis</span>
                                            <select is="emby-select" id="actionRecap" class="emby-select-withcolor emby-select">
                                                <option value="Default">Default</option>
                                                <option value="Chapter">Chapter</option>
                                                <option value="None">None</option>
                                            </select>
                                        </label>
                                        <label for="actionIntro" style="width: 23%">
                                            <span>Introduction analysis</span>
                                            <select is="emby-select" id="actionIntro" class="emby-select-withcolor emby-select">
                                                <option value="Default">Default</option>
                                                <option value="Chapter">Chapter</option>
                                                <option value="Chromaprint">Chromaprint</option>
                                                <option value="None">None</option>
                                            </select>
                                        </label>
                                        <label for="actionCredits" style="width: 23%">
                                            <span>Credits (Outro) analysis</span>
                                            <select is="emby-select" id="actionCredits" class="emby-select-withcolor emby-select">
                                                <option value="Default">Default</option>
                                                <option value="Chapter">Chapter</option>
                                                <option value="Chromaprint">Chromaprint</option>
                                                <option value="BlackFrame">BlackFrame</option>
                                                <option value="None">None</option>
                                            </select>
                                        </label>
                                        <label for="actionPreview" style="width: 23%">
                                            <span>Preview analysis</span>
                                            <select is="emby-select" id="actionPreview" class="emby-select-withcolor emby-select">
                                                <option value="Default">Default</option>
                                                <option value="Chapter">Chapter</option>
                                                <option value="None">None</option>
                                            </select>
                                        </label>
                                    </div>
                                    <p>
                                        <button is="emby-button" id="saveAnalyzerActions" class="raised button-submit block emby-button" style="display: none">Apply Changes</button>
                                    </p>
                                    <br />
                                </div>

                                <div id="episodeSelection" style="display: none">
                                    <p>
                                        <label class="inputLabel" for="troubleshooterEpisode1">Select a first and second episode</label>
                                        <select is="emby-select" id="troubleshooterEpisode1" class="emby-select-withcolor emby-select"></select>
                                        <label class="inputLabel" for="troubleshooterEpisode2"> </label>
                                        <select is="emby-select" id="troubleshooterEpisode2" class="emby-select-withcolor emby-select"></select>
                                    </p>
                                </div>

                                <div id="timestampEditor" style="display: none">
                                    <br />
                                    <h3 style="margin: 0">Introduction timestamp editor</h3>
                                    <br />
                                    <h4 style="margin: 0" id="editLeftEpisodeTitle"></h4>
                                    <br />
                                    <div class="inlineForm">
                                        <div class="inputContainer">
                                            <label class="inputLabel inputLabelUnfocused">Recap Start</label>
                                            <input type="text" id="editLeftRecapEpisodeStartDisplay" class="emby-input custom-time-input" readonly />
                                            <input type="number" id="editLeftRecapEpisodeStartEdit" class="emby-input custom-time-input" style="display: none" step="any" min="0" />
                                        </div>
                                        <div class="inputContainer">
                                            <label class="inputLabel inputLabelUnfocused">Recap End</label>
                                            <input type="text" id="editLeftRecapEpisodeEndDisplay" class="emby-input custom-time-input" readonly />
                                            <input type="number" id="editLeftRecapEpisodeEndEdit" class="emby-input custom-time-input" style="display: none" step="any" min="0" />
                                        </div>
                                    </div>
                                    <div class="inlineForm">
                                        <div class="inputContainer">
                                            <label class="inputLabel inputLabelUnfocused">Intro Start</label>
                                            <input type="text" id="editLeftIntroEpisodeStartDisplay" class="emby-input custom-time-input" readonly />
                                            <input type="number" id="editLeftIntroEpisodeStartEdit" class="emby-input custom-time-input" style="display: none" step="any" min="0" />
                                        </div>
                                        <div class="inputContainer">
                                            <label class="inputLabel inputLabelUnfocused">Intro End</label>
                                            <input type="text" id="editLeftIntroEpisodeEndDisplay" class="emby-input custom-time-input" readonly />
                                            <input type="number" id="editLeftIntroEpisodeEndEdit" class="emby-input custom-time-input" style="display: none" step="any" min="0" />
                                        </div>
                                    </div>
                                    <div class="inlineForm">
                                        <div class="inputContainer">
                                            <label class="inputLabel inputLabelUnfocused">Credits (Outro) Start</label>
                                            <input type="text" id="editLeftCreditEpisodeStartDisplay" class="emby-input custom-time-input" readonly />
                                            <input type="number" id="editLeftCreditEpisodeStartEdit" class="emby-input custom-time-input" style="display: none" step="any" min="0" />
                                        </div>
                                        <div class="inputContainer">
                                            <label class="inputLabel inputLabelUnfocused">Credits (Outro) End</label>
                                            <input type="text" id="editLeftCreditEpisodeEndDisplay" class="emby-input custom-time-input" readonly />
                                            <input type="number" id="editLeftCreditEpisodeEndEdit" class="emby-input custom-time-input" style="display: none" step="any" min="0" />
                                        </div>
                                    </div>
                                    <div class="inlineForm">
                                        <div class="inputContainer">
                                            <label class="inputLabel inputLabelUnfocused">Preview Start</label>
                                            <input type="text" id="editLeftPreviewEpisodeStartDisplay" class="emby-input custom-time-input" readonly />
                                            <input type="number" id="editLeftPreviewEpisodeStartEdit" class="emby-input custom-time-input" style="display: none" step="any" min="0" />
                                        </div>
                                        <div class="inputContainer">
                                            <label class="inputLabel inputLabelUnfocused">Preview End</label>
                                            <input type="text" id="editLeftPreviewEpisodeEndDisplay" class="emby-input custom-time-input" readonly />
                                            <input type="number" id="editLeftPreviewEpisodeEndEdit" class="emby-input custom-time-input" style="display: none" step="any" min="0" />
                                        </div>
                                    </div>
                                    <div id="rightEpisodeEditor">
                                        <br />
                                        <h4 style="margin: 0" id="editRightEpisodeTitle"></h4>
                                        <br />
                                        <div class="inlineForm">
                                            <div class="inputContainer">
                                                <label class="inputLabel inputLabelUnfocused">Recap Start</label>
                                                <input type="text" id="editRightRecapEpisodeStartDisplay" class="emby-input custom-time-input" readonly />
                                                <input type="number" id="editRightRecapEpisodeStartEdit" class="emby-input custom-time-input" style="display: none" step="any" min="0" />
                                            </div>
                                            <div class="inputContainer">
                                                <label class="inputLabel inputLabelUnfocused">Recap End</label>
                                                <input type="text" id="editRightRecapEpisodeEndDisplay" class="emby-input custom-time-input" readonly />
                                                <input type="number" id="editRightRecapEpisodeEndEdit" class="emby-input custom-time-input" style="display: none" step="any" min="0" />
                                            </div>
                                        </div>
                                        <div class="inlineForm">
                                            <div class="inputContainer">
                                                <label class="inputLabel inputLabelUnfocused">Intro Start</label>
                                                <input type="text" id="editRightIntroEpisodeStartDisplay" class="emby-input custom-time-input" readonly />
                                                <input type="number" id="editRightIntroEpisodeStartEdit" class="emby-input custom-time-input" style="display: none" step="any" min="0" />
                                            </div>
                                            <div class="inputContainer">
                                                <label class="inputLabel inputLabelUnfocused">Intro End</label>
                                                <input type="text" id="editRightIntroEpisodeEndDisplay" class="emby-input custom-time-input" readonly />
                                                <input type="number" id="editRightIntroEpisodeEndEdit" class="emby-input custom-time-input" style="display: none" step="any" min="0" />
                                            </div>
                                        </div>
                                        <div class="inlineForm">
                                            <div class="inputContainer">
                                                <label class="inputLabel inputLabelUnfocused">Credits (Outro) Start</label>
                                                <input type="text" id="editRightCreditEpisodeStartDisplay" class="emby-input custom-time-input" readonly />
                                                <input type="number" id="editRightCreditEpisodeStartEdit" class="emby-input custom-time-input" style="display: none" step="any" min="0" />
                                            </div>
                                            <div class="inputContainer">
                                                <label class="inputLabel inputLabelUnfocused">Credits (Outro) End</label>
                                                <input type="text" id="editRightCreditEpisodeEndDisplay" class="emby-input custom-time-input" readonly />
                                                <input type="number" id="editRightCreditEpisodeEndEdit" class="emby-input custom-time-input" style="display: none" step="any" min="0" />
                                            </div>
                                        </div>
                                        <div class="inlineForm">
                                            <div class="inputContainer">
                                                <label class="inputLabel inputLabelUnfocused">Preview Start</label>
                                                <input type="text" id="editRightPreviewEpisodeStartDisplay" class="emby-input custom-time-input" readonly />
                                                <input type="number" id="editRightPreviewEpisodeStartEdit" class="emby-input custom-time-input" style="display: none" step="any" min="0" />
                                            </div>
                                            <div class="inputContainer">
                                                <label class="inputLabel inputLabelUnfocused">Preview End</label>
                                                <input type="text" id="editRightPreviewEpisodeEndDisplay" class="emby-input custom-time-input" readonly />
                                                <input type="number" id="editRightPreviewEpisodeEndEdit" class="emby-input custom-time-input" style="display: none" step="any" min="0" />
                                            </div>
                                        </div>
                                    </div>
                                    <p style="margin: 20px 0">
                                        <button is="emby-button" id="btnUpdateTimestamps" class="raised button-submit block emby-button" type="button">Update timestamps</button>
                                    </p>
                                </div>
                                <p>
                                    <button is="emby-button" id="scanSeason" class="raised button-submit block emby-button" style="display: none">Scan Season</button>
                                </p>

                                <div id="timestampErrorDiv" style="display: none">
                                    <p>
                                        <textarea id="timestampError" rows="2" cols="75" readonly></textarea>
                                    </p>
                                </div>

                                <div id="fingerprintVisualizer" style="display: none">
                                    <hr style="border: none; border-top: 1px solid #ccc; margin: 20px 0" />

                                    <h3>Fingerprint Visualizer</h3>
                                    <p>
                                        Interactively compare the audio fingerprints of two episodes. <br />
                                        The blue and red bar to the right of the fingerprint diff turns blue when the corresponding fingerprint points are at least 80% similar.
                                    </p>
                                    <table>
                                        <thead>
                                            <tr>
                                                <td style="min-width: 100px; font-weight: bold">Key</td>
                                                <td style="font-weight: bold">Function</td>
                                            </tr>
                                        </thead>
                                        <tbody>
                                            <tr>
                                                <td>Up arrow</td>
                                                <td>Shift the left episode up by 0.1238 seconds. Holding control will shift the episode by 10 seconds.</td>
                                            </tr>
                                            <tr>
                                                <td>Down arrow</td>
                                                <td>Shift the left episode down by 0.1238 seconds. Holding control will shift the episode by 10 seconds.</td>
                                            </tr>
                                            <tr>
                                                <td>Right arrow</td>
                                                <td>Advance to the next pair of episodes.</td>
                                            </tr>
                                            <tr>
                                                <td>Left arrow</td>
                                                <td>Go back to the previous pair of episodes.</td>
                                            </tr>
                                        </tbody>
                                    </table>
                                    <br />

                                    <span>Shift amount:</span>
                                    <input type="number" min="-3000" max="3000" value="0" id="offset" />
                                    <p>
                                        <span id="suggestedShifts">
                                            <span>Suggested shifts: </span>
                                        </span>
                                    </p>
                                    <canvas id="troubleshooter" style="display: none"></canvas>
                                    <span id="timestampContainer">
                                        <span id="timestamps"></span>
                                        <br />
                                        <span id="intros"></span>
                                    </span>
                                    <br />
                                </div>

                                <div id="eraseSeasonContainer" style="display: none">
                                    <div class="checkboxContainer checkboxContainer" style="margin: 0">
                                        <label class="emby-checkbox-label">
                                            <input id="eraseSeasonCacheCheckbox" type="checkbox" is="emby-checkbox" />
                                            <span>Include cached fingerprint files</span>
                                        </label>
                                    </div>
                                    <button is="emby-button" id="btnEraseSeasonTimestamps" class="raised button-submit block emby-button" type="button">Erase all timestamps for this season</button>
                                </div>

                                <div id="eraseMovieContainer" style="display: none">
                                    <div class="checkboxContainer checkboxContainer" style="margin: 0">
                                        <label class="emby-checkbox-label">
                                            <input id="eraseMovieCacheCheckbox" type="checkbox" is="emby-checkbox" />
                                            <span>Include cached fingerprint files</span>
                                        </label>
                                    </div>
                                    <button is="emby-button" id="btnEraseMovieTimestamps" class="raised button-submit block emby-button" type="button">Erase all timestamps for this movie</button>
                                </div>

                                <hr style="border: none; border-top: 1px solid #ccc; margin: 20px 0" />

                                <div>
                                    <label class="selectLabel" for="GlobalTimestamps" style="display: none"></label>
                                    <select is="emby-select" id="GlobalTimestamps" class="emby-select-withcolor block emby-select">
                                        <option value="introduction">Global Introduction Timestamps</option>

                                        <option value="recap">Global Recap Timestamps</option>

                                        <option value="credits">Global Credits Timestamps</option>

                                        <option value="preview">Global Preview Timestamps</option>
                                    </select>
                                    <div class="checkboxContainer checkboxContainer" style="margin: 0">
                                        <label class="emby-checkbox-label">
                                            <input id="eraseModeCacheCheckbox" type="checkbox" is="emby-checkbox" />
                                            <span>Include global cached fingerprint files</span>
                                        </label>
                                    </div>
                                    <button is="emby-button" class="raised button-submit block emby-button" id="btnEraseGlobalTimestamps">Erase all selected timestamps (globally)</button>
                                </div>
                                <br />
                                <div>
                                    <p>
                                        <button is="emby-button" class="raised button-submit block emby-button" id="btnRebuildDatabase">Rebuild Local Database</button>
                                    </p>
                                    <p align="center">
                                        <b style="color: red">Rebuilding database requires a full Jellyfin restart to complete, <i>NOT</i> a dashboard restart!</b>
                                    </p>
                                </div>
                            </details>

                            <details id="support">
                                <summary>Intro Skipper Support Log</summary>

                                <textarea id="supportBundle" rows="20" cols="75" readonly></textarea>
                            </details>

                            <details id="storage">
                                <br />
                                <summary>Storage Usage</summary>
                                <div class="fieldDescription">See how much space each library uses.</div>
                                <textarea id="storageText" rows="20" cols="75" readonly></textarea>
                            </details>
                        </fieldset>
                    </form>
                </div>
            </div>

            <script src="configurationpage?name=visualizer.js"></script>

            <script>
                // Create namespace
                window.IntroSkipper = window.IntroSkipper || {};

                // Config module
                IntroSkipper.Config = (function() {
                    'use strict';

                    // Private variables
                    // first and second episodes to fingerprint & compare
                    let lhs = [];
                    let rhs = [];

                    // fingerprint point comparison & miminum similarity threshold (at most 6 bits out of 32 can be different)
                    let fprDiffs = [];
                    const fprDiffMinimum = (1 - 6 / 32) * 100;

                    // seasons grouped by show
                    let shows = {};

                // settings elements
                const visualizer = document.querySelector("details#visualizer");
                const support = document.querySelector("details#support");
                const storage = document.querySelector("details#storage");
                const btnRebuildDatabase = document.querySelector("button#btnRebuildDatabase");

                // all plugin configuration fields that can be get or set with .value (i.e. strings or numbers).
                const configurationFields = [
                    // analysis
                    "MaxParallelism",
                    "SelectedLibraries",
                    "AdjustWindowInward",
                    "AdjustWindowOutward",
                    "EndSnapThreshold",
                    "ExcludeSeries",
                    "ClientList",
                    "AnalysisPercent",
                    "AnalysisLengthLimit",
                    "MinimumIntroDuration",
                    "MaximumIntroDuration",
                    "MinimumCreditsDuration",
                    "MaximumCreditsDuration",
                    "MaximumMovieCreditsDuration",
                    "MinimumRecapDuration",
                    "MaximumRecapDuration",
                    "MinimumPreviewDuration",
                    "MaximumPreviewDuration",
                    "ProcessPriority",
                    "ProcessThreads",
                    // playback
                    "IntroEndOffset",
                    "IntroStartOffset",
                    "AutoSkipDelay",
                    // internals
                    "SilenceDetectionMaximumNoise",
                    "SilenceDetectionMinimumDuration",
                    "BlackFrameMinimumPercentage",
                    "BlackFrameThreshold",
                    "ChapterAnalyzerIntroductionPattern",
                    "ChapterAnalyzerEndCreditsPattern",
                    "ChapterAnalyzerPreviewPattern",
                    "ChapterAnalyzerRecapPattern",
                    "TypeList",
                    // UI customization
                    "AutoSkipNotificationText",
                ];

                const booleanConfigurationFields = ["AutoDetectIntros", "AnalyzeMovies", "AnalyzeSeasonZero", "SelectAllLibraries", "UpdateMediaSegments", "UseAlternativeBlackFrameAnalyzer", "UseChapterMarkersBlackFrame", "FullLengthChapters", "RebuildMediaSegments", "ScanIntroduction", "ScanCredits", "ScanRecap", "ScanPreview", "PreferChromaprint", "CacheFingerprints", "PluginSkip", "AutoSkip", "SkipFirstEpisode", "SnapToKeyframe", "AdjustIntroBasedOnSilence", "AdjustIntroBasedOnChapters"];

                // visualizer elements
                const analyzeMovies = document.getElementById("AnalyzeMovies");
                const analyzerActionsSection = document.querySelector("div#analyzerActionsSection");
                const actionIntro = analyzerActionsSection.querySelector("select#actionIntro");
                const actionCredits = analyzerActionsSection.querySelector("select#actionCredits");
                const actionRecap = analyzerActionsSection.querySelector("select#actionRecap");
                const actionPreview = analyzerActionsSection.querySelector("select#actionPreview");
                const saveAnalyzerActionsButton = analyzerActionsSection.querySelector("button#saveAnalyzerActions");
                const scanSeasonButton = document.querySelector("button#scanSeason");
                const canvas = document.querySelector("canvas#troubleshooter");
                const selectShow = document.querySelector("select#troubleshooterShow");
                const seasonSelection = document.getElementById("seasonSelection");
                const selectSeason = document.querySelector("select#troubleshooterSeason");
                const episodeSelection = document.getElementById("episodeSelection");
                const selectEpisode1 = document.querySelector("select#troubleshooterEpisode1");
                const selectEpisode2 = document.querySelector("select#troubleshooterEpisode2");
                const txtOffset = document.querySelector("input#offset");
                const txtSuggested = document.querySelector("span#suggestedShifts");
                const btnSeasonEraseTimestamps = document.querySelector("button#btnEraseSeasonTimestamps");
                const eraseSeasonContainer = document.getElementById("eraseSeasonContainer");
                const btnMovieEraseTimestamps = document.querySelector("button#btnEraseMovieTimestamps");
                const eraseMovieContainer = document.getElementById("eraseMovieContainer");
                const timestampError = document.querySelector("textarea#timestampError");
                const timestampEditor = document.querySelector("#timestampEditor");
                const rightEpisodeEditor = document.getElementById("rightEpisodeEditor");
                const btnUpdateTimestamps = document.querySelector("button#btnUpdateTimestamps");
                const timeContainer = document.querySelector("span#timestampContainer");
                const fingerprintVisualizer = document.getElementById("fingerprintVisualizer");
                const silenceSettings = document.getElementById("silenceSettings");

                let windowHashInterval = 0;

                const pluginSkip = document.getElementById("PluginSkip");
                const serverSkipSettings = document.getElementById("ServerSkipSettings");
                const autoSkip = document.getElementById("AutoSkip");
                const selectAllLibraries = document.querySelector("input#SelectAllLibraries");
                const librariesContainer = document.querySelector("div.folderAccessListContainer");
                const autoSkipClientList = document.querySelector("div.AutoSkipClientListContainer");
                const fullLengthChapters = document.getElementById("FullLengthChapters");
                const useAlternativeBlackFrameAnalyzer = document.getElementById("UseAlternativeBlackFrameAnalyzer");
                const chapterMarkersBlackFrameSetting = document.getElementById("ChapterMarkersBlackFrameSetting");
                const snapToKeyframe = document.getElementById("SnapToKeyframe");
                const adjustIntroBasedOnSilence = document.getElementById("AdjustIntroBasedOnSilence");
                const adjustIntroBasedOnChapters = document.getElementById("AdjustIntroBasedOnChapters");
                const globalTimestamps = document.getElementById("GlobalTimestamps");
                const btnEraseGlobalTimestamps = document.getElementById("btnEraseGlobalTimestamps");

                function autoSkipChanged() {
                    if (autoSkip.checked) {
                        autoSkipClientList.style.display = "none";
                    } else {
                        autoSkipClientList.style.display = "unset";
                        autoSkipClientList.style.width = "100%";
                    }
                }

                autoSkip.addEventListener("change", autoSkipChanged);

                function selectAllLibrariesChanged() {
                    if (selectAllLibraries.checked) {
                        librariesContainer.style.display = "none";
                    } else {
                        librariesContainer.style.display = "unset";
                    }
                }
                selectAllLibraries.addEventListener("change", selectAllLibrariesChanged);

                const recapPreviewDurations = document.getElementById("RecapPreviewDurations");

                // Hide or show durations for segments that are exclusively analyzed by chapters
                function fullLengthChaptersChanged() {
                    if (fullLengthChapters.checked) {
                        recapPreviewDurations.style.display = "none";
                    } else {
                        recapPreviewDurations.style.display = "unset";
                    }
                }

                fullLengthChapters.addEventListener("change", fullLengthChaptersChanged);

                function updateList(textField, container) {
                    textField.value = Array.from(container.querySelectorAll('input[type="checkbox"]:checked'))
                        .map((checkbox) => checkbox.nextElementSibling.textContent)
                        .join(", ");
                }

                function generateCheckboxList(items, containerId, textFieldId) {
                    const container = document.getElementById(containerId);
                    const checkedItems = new Set(document.getElementById(textFieldId).value.split(", ").filter(Boolean));
                    const fragment = document.createDocumentFragment();
                    for (const item of items) {
                        const label = document.createElement("label");
                        label.className = "emby-checkbox-label";
                        label.innerHTML = '<input type="checkbox" is="emby-checkbox"' + (checkedItems.has(item) ? " checked" : "") + ">" + '<span class="checkboxLabel">' + item + "</span>";
                        fragment.appendChild(label);
                    }
                    container.innerHTML = "";
                    container.appendChild(fragment);
                    container.addEventListener(
                        "change",
                        (e) => {
                            if (e.target.type === "checkbox") updateList(document.getElementById(textFieldId), container);
                        },
                        { passive: true },
                    );
                }

                async function generateAutoSkipClientList() {
                    const response = await getJson("Devices");
                    const devices = [...new Set(response.Items.map((item) => item.AppName))];
                    generateCheckboxList(devices, "autoSkipCheckboxes", "ClientList");
                }

                async function generateAutoSkipTypeList() {
                    const types = ["Introduction", "Credits", "Recap", "Preview"];
                    generateCheckboxList(types, "autoSkipTypeCheckboxes", "TypeList");
                }

                const skipFirstEpisode = document.getElementById("divSkipFirstEpisode");
                const IntroStartOffset = document.getElementById("IntroStartOffset");
                const AutoSkipDelay = document.getElementById("AutoSkipDelay");

                async function populateLibraries() {
                    const response = await getJson("Library/VirtualFolders");
                    const tvLibraries = response.filter((item) => item.CollectionType === undefined || item.CollectionType === "tvshows" || item.CollectionType === "movies");
                    const libraryNames = tvLibraries.map((lib) => lib.Name || "Unnamed Library");
                    generateCheckboxList(libraryNames, "libraryCheckboxes", "SelectedLibraries");
                }

                async function pluginSkipSettingVisible() {
                    if (pluginSkip.checked || autoSkip.checked) {
                        pluginSkip.checked = true;
                        serverSkipSettings.style.display = "unset";
                    } else {
                        serverSkipSettings.style.display = "none";
                    }
                }

                async function alternativeBlackFrameAnalyzerSettingsVisible() {
                    if (useAlternativeBlackFrameAnalyzer.checked) {
                        chapterMarkersBlackFrameSetting.style.display = "none";
                    } else {
                        chapterMarkersBlackFrameSetting.style.display = "unset";
                    }
                }

                useAlternativeBlackFrameAnalyzer.addEventListener("change", alternativeBlackFrameAnalyzerSettingsVisible);

                async function adjustSettingsVisible() {
                    if (adjustIntroBasedOnSilence.checked) {
                        silenceSettings.style.display = "unset";
                    } else {
                        silenceSettings.style.display = "none";
                    }
                }

                adjustIntroBasedOnSilence.addEventListener("change", adjustSettingsVisible);

                async function pluginSkipSettingChanged() {
                    if (!pluginSkip.checked) {
                        autoSkip.checked = false;
                        skipFirstEpisode.checked = false;
                        IntroStartOffset.value = 0;
                    }
                    pluginSkipSettingVisible();
                }

                pluginSkip.addEventListener("change", pluginSkipSettingChanged);

                async function analyzeMoviesChanged() {
                    if (analyzeMovies.checked) {
                        movieCreditsDuration.style.display = "unset";
                    } else {
                        movieCreditsDuration.style.display = "none";
                    }
                }
                analyzeMovies.addEventListener("change", analyzeMoviesChanged);

                function globalTimestampChanged() {
                    btnEraseGlobalTimestamps.textContent = "Erase all " + globalTimestamps.value + " timestamps (globally)";
                }

                globalTimestamps.addEventListener("change", globalTimestampChanged);
                // https://stackoverflow.com/a/72161208/461982
                globalTimestamps.addEventListener("click", () => {
                    globalTimestamps.removeEventListener("change", globalTimestampChanged);
                    globalTimestamps.addEventListener("change", globalTimestampChanged);
                });

                // when the fingerprint visualizer opens, populate show names
                async function visualizerToggled() {
                    if (!visualizer.open) {
                        analyzerActionsSection.style.display = "none";
                        saveAnalyzerActionsButton.style.display = "none";
                        scanSeasonButton.style.display = "none";
                        return;
                    }

                    // ensure the series select is empty
                    selectShow.innerHTML = "";

                    Dashboard.showLoadingMsg();
                    shows = await getJson("Intros/Shows");

                    // Create an object to store shows by library
                    let showsByLibrary = {};

                    // Categorize shows by LibraryName
                    for (const show in shows) {
                        const libraryName = shows[show].LibraryName || "Uncategorized";
                        if (!showsByLibrary[libraryName]) {
                            showsByLibrary[libraryName] = [];
                        }
                        showsByLibrary[libraryName].push({
                            value: show,
                            text: shows[show].SeriesName + " (" + shows[show].ProductionYear + ")",
                        });
                    }

                    // Add categorized shows to the select element
                    for (const library in showsByLibrary) {
                        const optgroup = document.createElement("optgroup");
                        optgroup.label = library;

                        showsByLibrary[library].forEach((show) => {
                            const option = document.createElement("option");
                            option.value = show.value;
                            option.textContent = show.text;
                            optgroup.appendChild(option);
                        });

                        selectShow.appendChild(optgroup);
                    }

                    selectShow.value = "";
                    Dashboard.hideLoadingMsg();
                }

                // fetch the support bundle whenever the detail section is opened.
                async function supportToggled() {
                    if (!support.open) {
                        return;
                    }

                    // Fetch the support bundle
                    const bundle = await fetchWithAuth("IntroSkipper/SupportBundle", "GET", null);
                    const bundleText = await bundle.text();

                    // Display it to the user and select all
                    const ta = document.querySelector("textarea#supportBundle");
                    ta.value = bundleText;
                    ta.focus();
                    ta.setSelectionRange(0, ta.value.length);

                    // Attempt to copy it to the clipboard automatically, falling back
                    // to prompting the user to press Ctrl + C.
                    try {
                        navigator.clipboard.writeText(bundleText);
                        Dashboard.alert("Support bundle copied to clipboard");
                    } catch {
                        Dashboard.alert("Press Ctrl+C to copy support bundle");
                    }
                }

                // fetch the storage whenever the detail section is opened.
                async function storageToggled() {
                    if (!storage.open) {
                        return;
                    }

                    // Fetch the support bundle
                    const bundle = await fetchWithAuth("IntroSkipper/Storage", "GET", null);
                    const bundleText = await bundle.text();

                    // Display it to the user
                    const ta = document.querySelector("textarea#storageText");
                    ta.value = bundleText;
                }

                // show changed, populate seasons
                async function showChanged() {
                    seasonSelection.style.display = "unset";
                    clearSelect(selectSeason);
                    eraseSeasonContainer.style.display = "none";
                    eraseMovieContainer.style.display = "none";
                    episodeSelection.style.display = "unset";
                    clearSelect(selectEpisode1);
                    clearSelect(selectEpisode2);

                    if (shows[selectShow.value].IsMovie) {
                        await movieLoaded();
                        return;
                    }

                    // add all seasons from this show to the season select
                    for (const season in shows[selectShow.value].Seasons) {
                        addItem(selectSeason, "Season " + shows[selectShow.value].Seasons[season], season);
                    }

                    selectSeason.value = "";
                }

                // season changed, reload all episodes
                async function seasonChanged() {
                    const seasonData = encodeURI(selectShow.value) + "/" + encodeURI(selectSeason.value);
                    Dashboard.showLoadingMsg();
                    // show the analyzer actions editor.
                    saveAnalyzerActionsButton.style.display = "block";
                    saveAnalyzerActionsButton.textContent = "Apply to season";
                    scanSeasonButton.style.display = "block";
                    scanSeasonButton.textContent = "Scan season";
                    const analyzerActions = await getJson("Intros/AnalyzerActions/" + encodeURI(selectSeason.value));
                    actionIntro.value = analyzerActions.Introduction || "Default";
                    actionCredits.value = analyzerActions.Credits || "Default";
                    actionRecap.value = analyzerActions.Recap || "Default";
                    actionPreview.value = analyzerActions.Preview || "Default";
                    analyzerActionsSection.style.display = "unset";

                    // show the erase season button
                    eraseSeasonContainer.style.display = "unset";

                    clearSelect(selectEpisode1);
                    clearSelect(selectEpisode2);
                    let i = 1;
                    const episodes = await getJson("Intros/Show/" + seasonData);
                    for (const episode in episodes) {
                        const strI = i.toLocaleString("en", {
                            minimumIntegerDigits: 2,
                            maximumFractionDigits: 0,
                        });
                        addItem(selectEpisode1, strI + ": " + episodes[episode].Name, episodes[episode].Id);
                        addItem(selectEpisode2, strI + ": " + episodes[episode].Name, episodes[episode].Id);
                        i++;
                    }
                    Dashboard.hideLoadingMsg();

                    setTimeout(() => {
                        selectEpisode1.selectedIndex = 0;
                        selectEpisode2.selectedIndex = 1;
                        episodeChanged();
                    }, 100);
                }

                // episode changed, get fingerprints & calculate diff
                async function episodeChanged() {
                    if (!selectEpisode1.value || !selectEpisode2.value) {
                        return;
                    }

                    Dashboard.showLoadingMsg();

                    timestampError.value = "";
                    fingerprintVisualizer.style.display = "unset";
                    canvas.style.display = "none";

                    lhs = await getJson("Intros/Episode/" + selectEpisode1.value + "/Chromaprint");
                    if (lhs === undefined) {
                        timestampError.value += "Error: " + selectEpisode1.value + " fingerprints failed!\n";
                    } else if (lhs === null) {
                        timestampError.value += selectEpisode1.value + " fingerprints missing or incomplete.\n";
                    }

                    rightEpisodeEditor.style.display = "unset";

                    rhs = await getJson("Intros/Episode/" + selectEpisode2.value + "/Chromaprint");
                    if (rhs === undefined) {
                        timestampError.value += "Error: " + selectEpisode2.value + " fingerprints failed!";
                    } else if (rhs === null) {
                        timestampError.value += selectEpisode2.value + " fingerprints missing or incomplete.\n";
                    }

                    if (timestampError.value === "") {
                        timestampErrorDiv.style.display = "none";
                    } else {
                        timestampErrorDiv.style.display = "unset";
                    }

                    Dashboard.hideLoadingMsg();

                    txtOffset.value = "0";
                    IntroSkipper.Visualizer.refreshBounds();
                    IntroSkipper.Visualizer.renderTroubleshooter();
                    IntroSkipper.Visualizer.findExactMatches();
                    await updateTimestampEditor();
                }

                async function movieLoaded() {
                    Dashboard.showLoadingMsg();

                    saveAnalyzerActionsButton.textContent = "Apply to movie";
                    scanSeasonButton.textContent = "Scan movie";
                    seasonSelection.style.display = "none";
                    episodeSelection.style.display = "none";
                    eraseMovieContainer.style.display = "unset";
                    scanSeasonButton.style.display = "block";

                    timestampError.value = "";
                    fingerprintVisualizer.style.display = "none";
                    rightEpisodeEditor.style.display = "none";

                    if (timestampError.value === "") {
                        timestampErrorDiv.style.display = "none";
                    } else {
                        timestampErrorDiv.style.display = "unset";
                    }

                    Dashboard.hideLoadingMsg();

                    txtOffset.value = "0";

                    await updateTimestampEditor();
                }

                function setupTimeInputs() {
                    const timestampEditor = document.getElementById("timestampEditor");
                    timestampEditor.querySelectorAll(".inputContainer").forEach((container) => {
                        const displayInput = container.querySelector('[id$="Display"]');
                        const editInput = container.querySelector('[id$="Edit"]');
                        displayInput.addEventListener("pointerdown", (e) => {
                            e.preventDefault();
                            switchToEdit(displayInput, editInput);
                        });
                        editInput.addEventListener("blur", () => switchToDisplay(displayInput, editInput));
                        displayInput.value = formatTime(parseFloat(editInput.value) || 0);
                    });
                }

                function switchToEdit(displayInput, editInput) {
                    displayInput.style.display = "none";
                    editInput.style.display = "";
                    editInput.focus();
                }

                function switchToDisplay(displayInput, editInput) {
                    editInput.style.display = "none";
                    displayInput.style.display = "";
                    displayInput.value = formatTime(parseFloat(editInput.value) || 0);
                }

                function formatTime(totalSeconds) {
                    const hours = Math.floor(totalSeconds / 3600);
                    const minutes = Math.floor((totalSeconds % 3600) / 60);
                    const seconds = Math.floor(totalSeconds % 60);
                    let result = [];
                    if (hours > 0) result.push(hours + " hour" + (hours !== 1 ? "s" : ""));
                    if (minutes > 0) result.push(minutes + " minute" + (minutes !== 1 ? "s" : ""));
                    if (seconds > 0 || result.length === 0) result.push(seconds + " second" + (seconds !== 1 ? "s" : ""));
                    return result.join(" ");
                }

                // updates the timestamp editor
                async function updateTimestampEditor() {
                    // Get the title and ID of the left and right episodes
                    const leftEpisode = selectEpisode1.options[selectEpisode1.selectedIndex] || selectShow;

                    // Try to get the timestamps of each intro, falling back a default value of zero if no intro was found
                    const leftEpisodeJson = await getJson("Episode/" + leftEpisode.value + "/Timestamps");

                    // Update the editor for the first episode
                    timestampEditor.style.display = "unset";
                    document.querySelector("#editLeftEpisodeTitle").textContent = leftEpisode.text;
                    document.querySelector("#editLeftIntroEpisodeStartEdit").value = leftEpisodeJson.Introduction.Start;
                    document.querySelector("#editLeftIntroEpisodeEndEdit").value = leftEpisodeJson.Introduction.End;
                    document.querySelector("#editLeftCreditEpisodeStartEdit").value = leftEpisodeJson.Credits.Start;
                    document.querySelector("#editLeftCreditEpisodeEndEdit").value = leftEpisodeJson.Credits.End;
                    document.querySelector("#editLeftRecapEpisodeStartEdit").value = leftEpisodeJson.Recap.Start;
                    document.querySelector("#editLeftRecapEpisodeEndEdit").value = leftEpisodeJson.Recap.End;
                    document.querySelector("#editLeftPreviewEpisodeStartEdit").value = leftEpisodeJson.Preview.Start;
                    document.querySelector("#editLeftPreviewEpisodeEndEdit").value = leftEpisodeJson.Preview.End;

                    // Update the editor for the second episode
                    if (rightEpisodeEditor.style.display !== "none") {
                        const rightEpisode = selectEpisode2.options[selectEpisode2.selectedIndex];

                        // Try to get the timestamps of each intro, falling back a default value of zero if no intro was found
                        const rightEpisodeJson = await getJson("Episode/" + rightEpisode.value + "/Timestamps");

                        // Update the editor for the second episode
                        document.querySelector("#editRightEpisodeTitle").textContent = rightEpisode.text;
                        document.querySelector("#editRightIntroEpisodeStartEdit").value = rightEpisodeJson.Introduction.Start;
                        document.querySelector("#editRightIntroEpisodeEndEdit").value = rightEpisodeJson.Introduction.End;
                        document.querySelector("#editRightCreditEpisodeStartEdit").value = rightEpisodeJson.Credits.Start;
                        document.querySelector("#editRightCreditEpisodeEndEdit").value = rightEpisodeJson.Credits.End;
                        document.querySelector("#editRightRecapEpisodeStartEdit").value = rightEpisodeJson.Recap.Start;
                        document.querySelector("#editRightRecapEpisodeEndEdit").value = rightEpisodeJson.Recap.End;
                        document.querySelector("#editRightPreviewEpisodeStartEdit").value = rightEpisodeJson.Preview.Start;
                        document.querySelector("#editRightPreviewEpisodeEndEdit").value = rightEpisodeJson.Preview.End;
                    }

                    // Update display inputs
                    const inputs = document.querySelectorAll('#timestampEditor input[type="number"]');
                    inputs.forEach((input) => {
                        const displayInput = document.getElementById(input.id.replace("Edit", "Display"));
                        displayInput.value = formatTime(parseFloat(input.value) || 0);
                    });

                    setupTimeInputs();
                }

                // adds an item to a dropdown
                function addItem(select, text, value) {
                    let item = new Option(text, value);
                    select.add(item);
                }

                // clear selection of items
                function clearSelect(select) {
                    timestampError.value = "";
                    timestampErrorDiv.style.display = "none";
                    timestampEditor.style.display = "none";
                    timeContainer.style.display = "none";
                    canvas.style.display = "none";
                    let i,
                        L = select.options.length - 1;
                    for (i = L; i >= 0; i--) {
                        select.remove(i);
                    }
                }

                // make an authenticated GET to the server and parse the response as JSON
                async function getJson(url) {
                    return await fetchWithAuth(url, "GET")
                        .then((r) => {
                            if (r.ok) {
                                return r.json();
                            } else {
                                return null;
                            }
                        })
                        .catch((err) => {
                            console.debug(err);
                        });
                }

                // make an authenticated fetch to the server
                async function fetchWithAuth(url, method, body) {
                    url = ApiClient.serverAddress() + "/" + url;

                    const reqInit = {
                        method: method,
                        headers: {
                            Authorization: "MediaBrowser Token=" + ApiClient.accessToken(),
                        },
                        body: body,
                    };

                    if (method === "POST") {
                        reqInit.headers["Content-Type"] = "application/json";
                    }

                    return await fetch(url, reqInit);
                }

                // key pressed
                function keyDown(e) {
                    let episodeDelta = 0;
                    let offsetDelta = 0;

                    switch (e.key) {
                        case "ArrowDown":
                            if (timestampError.value !== "") {
                                // if the control key is pressed, shift LHS by 10s. Otherwise, shift by 1.
                                offsetDelta = e.ctrlKey ? 10 / 0.1238 : 1;
                            }
                            break;

                        case "ArrowUp":
                            if (timestampError.value !== "") {
                                offsetDelta = e.ctrlKey ? -10 / 0.1238 : -1;
                            }
                            break;

                        case "ArrowRight":
                            episodeDelta = 2;
                            break;

                        case "ArrowLeft":
                            episodeDelta = -2;
                            break;

                        default:
                            return;
                    }

                    if (offsetDelta !== 0) {
                        txtOffset.value = Number(txtOffset.value) + Math.floor(offsetDelta);
                    }

                    if (episodeDelta !== 0) {
                        // calculate the number of episodes remaining in the LHS and RHS episode pickers
                        const lhsRemaining = selectEpisode1.selectedIndex;
                        const rhsRemaining = selectEpisode2.length - selectEpisode2.selectedIndex - 1;

                        // if we're moving forward and the right episode picker is close to the end, don't move.
                        if (episodeDelta > 0 && rhsRemaining <= 1) {
                            return;
                        } else if (episodeDelta < 0 && lhsRemaining <= 1) {
                            return;
                        }

                        selectEpisode1.selectedIndex += episodeDelta;
                        selectEpisode2.selectedIndex += episodeDelta;
                        episodeChanged();
                    }

                    IntroSkipper.Visualizer.renderTroubleshooter();
                    e.preventDefault();
                }

                // check that the user is still on the configuration page
                function checkWindowHash() {
                    const h = location.hash;
                    if (h === "#!/configurationpage?name=Intro%20Skipper" || h.includes("#!/dialog")) {
                        return;
                    }

                    console.debug("navigated away from intro skipper configuration page");
                    document.removeEventListener("keydown", keyDown);
                    clearInterval(windowHashInterval);
                }

                // converts seconds to a readable timestamp (i.e. 127 becomes "02:07").
                function secondsToString(seconds) {
                    return new Date(seconds * 1000).toISOString().slice(14, 19);
                }

                // erase all intro/credits timestamps
                function eraseTimestamps(mode) {
                    const lower = mode.toLocaleLowerCase();
                    const title = "Confirm timestamp erasure";
                    const body = "Are you sure you want to erase all previously discovered " + mode.toLocaleLowerCase() + " timestamps?";
                    const eraseCacheChecked = document.getElementById("eraseModeCacheCheckbox").checked;

                    Dashboard.confirm(body, title, (result) => {
                        if (!result) {
                            return;
                        }

                        fetchWithAuth("Intros/EraseTimestamps?mode=" + mode + "&eraseCache=" + eraseCacheChecked, "POST", null);

                        Dashboard.alert(mode + " timestamps erased");
                        document.getElementById("eraseModeCacheCheckbox").checked = false;
                    });
                }

                function loadConfiguration() {
                    Dashboard.showLoadingMsg();
                    ApiClient.getPluginConfiguration("c83d86bb-a1e0-4c35-a113-e2101cf4ee6b")
                        .then((config) => {
                            for (const field of configurationFields) {
                                document.querySelector("#" + field).value = config[field];
                            }

                            for (const field of booleanConfigurationFields) {
                                document.querySelector("#" + field).checked = config[field];
                            }

                            analyzeMoviesChanged();
                            populateLibraries();
                            selectAllLibrariesChanged();
                            fullLengthChaptersChanged();
                            autoSkipChanged();
                            generateAutoSkipTypeList();
                            generateAutoSkipClientList();
                            pluginSkipSettingVisible();
                            alternativeBlackFrameAnalyzerSettingsVisible();
                            adjustSettingsVisible();
                            globalTimestampChanged();

                            Dashboard.hideLoadingMsg();
                        })
                        .catch((error) => {
                            Dashboard.hideLoadingMsg();
                        });
                }

                // Initial load
                document.querySelector("#TemplateConfigPage").addEventListener("pageshow", loadConfiguration);

                document.querySelector("#FingerprintConfigForm").addEventListener("submit", (e) => {
                    Dashboard.showLoadingMsg();
                    ApiClient.getPluginConfiguration("c83d86bb-a1e0-4c35-a113-e2101cf4ee6b")
                        .then((config) => {
                            for (const field of configurationFields) {
                                config[field] = document.querySelector("#" + field).value;
                            }

                            for (const field of booleanConfigurationFields) {
                                config[field] = document.querySelector("#" + field).checked;
                            }

                            ApiClient.updatePluginConfiguration("c83d86bb-a1e0-4c35-a113-e2101cf4ee6b", config)
                                .then((result) => {
                                    Dashboard.hideLoadingMsg();
                                    Dashboard.processPluginConfigurationUpdateResult(result);
                                })
                                .catch((error) => {
                                    Dashboard.hideLoadingMsg();
                                });
                        })
                        .catch((error) => {
                            Dashboard.hideLoadingMsg();
                        });

                    e.preventDefault();
                    return false;
                });

                visualizer.addEventListener("toggle", visualizerToggled);
                support.addEventListener("toggle", supportToggled);
                storage.addEventListener("toggle", storageToggled);
                txtOffset.addEventListener("change", function() {
                    IntroSkipper.Visualizer.renderTroubleshooter();
                });
                selectShow.addEventListener("change", showChanged);
                selectSeason.addEventListener("change", seasonChanged);
                selectEpisode1.addEventListener("change", episodeChanged);
                selectEpisode2.addEventListener("change", episodeChanged);

                saveAnalyzerActionsButton.addEventListener("click", () => {
                    Dashboard.showLoadingMsg();

                    const url = "Intros/AnalyzerActions/UpdateSeason";
                    const actions = {
                        id: selectSeason.value,
                        analyzerActions: {
                            Introduction: actionIntro.value,
                            Credits: actionCredits.value,
                            Recap: actionRecap.value,
                            Preview: actionPreview.value,
                        },
                    };

                    fetchWithAuth(url, "POST", JSON.stringify(actions));

                    Dashboard.alert("Analyzer actions updated for " + selectSeason.value + " of " + selectShow.value);
                    Dashboard.hideLoadingMsg();
                });

                scanSeasonButton.addEventListener("click", async () => {
                    Dashboard.showLoadingMsg();
                    console.log("(Re-) Scan task started.");
                    await fetchWithAuth("Intros/ScanSeason/" + selectShow.value + "/" + (selectSeason.value || selectShow.value), "POST", null);
                    await updateTimestampEditor();
                    Dashboard.hideLoadingMsg();
                });

                btnUpdateTimestamps.addEventListener("click", () => {
                    const getEditValue = (id) => parseFloat(document.getElementById(id).value) || 0;

                    // Check if we're dealing with a movie or TV show
                    const isMovie = selectShow.value && shows[selectShow.value] && shows[selectShow.value].IsMovie;

                    let lhsId;
                    if (isMovie) {
                        // For movies, use the show ID directly
                        lhsId = selectShow.value;
                    } else {
                        // For TV shows, use the selected episode
                        const selectedOption = selectEpisode1.options[selectEpisode1.selectedIndex];
                        if (!selectedOption) {
                            Dashboard.alert("Please select an episode first");
                            return;
                        }
                        lhsId = selectedOption.value;
                    }

                    const newLhs = {
                        Introduction: {
                            ItemId: lhsId,
                            Start: getEditValue("editLeftIntroEpisodeStartEdit"),
                            End: getEditValue("editLeftIntroEpisodeEndEdit"),
                        },
                        Credits: {
                            ItemId: lhsId,
                            Start: getEditValue("editLeftCreditEpisodeStartEdit"),
                            End: getEditValue("editLeftCreditEpisodeEndEdit"),
                        },
                        Recap: {
                            ItemId: lhsId,
                            Start: getEditValue("editLeftRecapEpisodeStartEdit"),
                            End: getEditValue("editLeftRecapEpisodeEndEdit"),
                        },
                        Preview: {
                            ItemId: lhsId,
                            Start: getEditValue("editLeftPreviewEpisodeStartEdit"),
                            End: getEditValue("editLeftPreviewEpisodeEndEdit"),
                        },
                    };

                    // Save the first/left timestamps
                    fetchWithAuth("Episode/" + lhsId + "/Timestamps", "POST", JSON.stringify(newLhs));

                    // For TV shows, also save the second episode's timestamps
                    if (!isMovie && rightEpisodeEditor.style.display !== "none") {
                        const rhsSelectedOption = selectEpisode2.options[selectEpisode2.selectedIndex];
                        if (rhsSelectedOption) {
                            const rhsId = rhsSelectedOption.value;
                            const newRhs = {
                                Introduction: {
                                    ItemId: rhsId,
                                    Start: getEditValue("editRightIntroEpisodeStartEdit"),
                                    End: getEditValue("editRightIntroEpisodeEndEdit"),
                                },
                                Credits: {
                                    ItemId: rhsId,
                                    Start: getEditValue("editRightCreditEpisodeStartEdit"),
                                    End: getEditValue("editRightCreditEpisodeEndEdit"),
                                },
                                Recap: {
                                    ItemId: rhsId,
                                    Start: getEditValue("editRightRecapEpisodeStartEdit"),
                                    End: getEditValue("editRightRecapEpisodeEndEdit"),
                                },
                                Preview: {
                                    ItemId: rhsId,
                                    Start: getEditValue("editRightPreviewEpisodeStartEdit"),
                                    End: getEditValue("editRightPreviewEpisodeEndEdit"),
                                },
                            };
                            fetchWithAuth("Episode/" + rhsId + "/Timestamps", "POST", JSON.stringify(newRhs));
                        }
                    }

                    Dashboard.alert("New segment timestamps saved");
                });

                btnSeasonEraseTimestamps.addEventListener("click", () => {
                    Dashboard.confirm("Are you sure you want to erase all timestamps for this season?", "Confirm timestamp erasure", (result) => {
                        if (!result) {
                            return;
                        }

                        const show = selectShow.value;
                        const season = selectSeason.value;
                        const eraseCacheChecked = document.getElementById("eraseSeasonCacheCheckbox").checked;

                        const url = "Intros/Show/" + encodeURIComponent(show) + "/" + encodeURIComponent(season);
                        fetchWithAuth(url + "?eraseCache=" + eraseCacheChecked, "DELETE", null);

                        Dashboard.alert("Erased timestamps for " + season + " of " + show);
                        document.getElementById("eraseSeasonCacheCheckbox").checked = false;
                    });
                });

                btnMovieEraseTimestamps.addEventListener("click", () => {
                    Dashboard.confirm("Are you sure you want to erase all timestamps for this movie?", "Confirm timestamp erasure", (result) => {
                        if (!result) {
                            return;
                        }

                        const show = selectShow.value;
                        const eraseCacheChecked = document.getElementById("eraseMovieCacheCheckbox").checked;

                        const url = "Intros/Show/" + encodeURIComponent(show);
                        fetchWithAuth(url + "?eraseCache=" + eraseCacheChecked, "DELETE", null);

                        Dashboard.alert("Erased timestamps for " + show);
                        document.getElementById("eraseMovieCacheCheckbox").checked = false;
                    });
                });

                btnEraseGlobalTimestamps.addEventListener("click", (e) => {
                    switch (globalTimestamps.value) {
                        case "introduction":
                            eraseTimestamps("Introduction");
                            break;
                        case "recap":
                            eraseTimestamps("Recap");
                            break;
                        case "credits":
                            eraseTimestamps("Credits");
                            break;
                        case "preview":
                            eraseTimestamps("Preview");
                            break;
                        default:
                            return;
                    }
                    e.preventDefault();
                });

                btnRebuildDatabase.addEventListener("click", () => {
                    fetchWithAuth("Intros/RebuildDatabase", "POST", null);
                });

                document.addEventListener("keydown", keyDown);
                windowHashInterval = setInterval(checkWindowHash, 2500);

                canvas.addEventListener("mousemove", (e) => {
                    const rect = e.currentTarget.getBoundingClientRect();
                    const y = e.clientY - rect.top;
                    const shift = Number(txtOffset.value);

                    let lTime, rTime, diffPos;
                    if (shift < 0) {
                        lTime = y * 0.1238;
                        rTime = (y + shift) * 0.1238;
                        diffPos = y + shift;
                    } else {
                        lTime = (y - shift) * 0.1238;
                        rTime = y * 0.1238;
                        diffPos = y - shift;
                    }

                    const diff = fprDiffs[Math.floor(diffPos)];

                    if (!diff) {
                        timeContainer.style.display = "none";
                        return;
                    } else {
                        timeContainer.style.display = "unset";
                    }

                    const times = document.querySelector("span#timestamps");

                    // LHS timestamp, RHS timestamp, percent similarity
                    times.textContent = secondsToString(lTime) + ", " + secondsToString(rTime) + ", " + Math.round(diff) + "%";

                    timeContainer.style.position = "relative";
                    timeContainer.style.left = "25px";
                    timeContainer.style.top = (-1 * rect.height + y).toString() + "px";
                });

                function setTime(seconds) {
                    // Calculate hours, minutes, and remaining seconds
                    let hours = Math.floor(seconds / 3600);
                    let minutes = Math.floor((seconds % 3600) / 60);
                    let remainingSeconds = seconds % 60;

                    // Format as HH:MM:SS and set the value of the time input
                    return String(hours).padStart(2, "0") + ":" + String(minutes).padStart(2, "0") + ":" + String(remainingSeconds).padStart(2, "0");
                }

                function getTimeInSeconds(time) {
                    let [hours, minutes, seconds] = time.split(":").map(Number);
                    return hours * 3600 + minutes * 60 + seconds;
                }

                // Public API
                return {
                    // Expose state for visualizer
                    getState: function() {
                        return {
                            lhs: lhs,
                            rhs: rhs,
                            fprDiffs: fprDiffs,
                            fprDiffMinimum: fprDiffMinimum
                        };
                    },

                    // Expose DOM elements
                    getElements: function() {
                        return {
                            canvas: canvas,
                            txtOffset: txtOffset,
                            selectEpisode1: selectEpisode1,
                            selectEpisode2: selectEpisode2,
                            txtSuggested: txtSuggested,
                            timeContainer: timeContainer
                        };
                    },

                    // Expose utility functions
                    utils: {
                        secondsToString: secondsToString
                    },

                    // Setter for visualizer to update state
                    setFprDiffs: function(diffs) {
                        fprDiffs = diffs;
                    }
                };
                })();
            </script>
        </div>
    </body>
</html>
